﻿using System;
using System.IO;
using System.Linq;
using System.Net;
using System.Text;
using System.CodeDom.Compiler;
using System.CodeDom;
using System.Collections.Generic;
using System.Text.RegularExpressions;
using System.Threading;
using System.Reflection;
using System.Xml;
using System.Xml.Schema;
using Facebook.Api.Generator.Configuration;

namespace Facebook.Api.Generator
{
    /// <summary>A static class that contains logic for generating the .NET Facebook API client.</summary>
    public static class FacebookApiGenerator
    {
        /// <summary>Loads content from the specified url.</summary>
        /// <param name="url">The url to load content from.</param>
        /// <returns>A <see cref="String" /> value containing the content of the specified url.</returns>
        internal static String GetContentFromUri(String url)
        {
            return CachedWebContent.Load(url).Content;
        }

        /// <summary>Loads content from the specified file on the file system.</summary>
        /// <param name="path">The path of the file to load.</param>
        /// <returns>A <see cref="String" /> value containing the content of the specified file.</returns>
        internal static String GetContentFromFile(String path)
        {
            using (var stream = File.OpenRead(path))
            {
                using (var reader = new StreamReader(stream))
                {
                    return reader.ReadToEnd();
                }
            }
        }

        /// <summary>Loads content from the specified file or url.</summary>
        /// <param name="pathOrUrl">The path or url of the file to load.</param>
        /// <returns>A <see cref="String" /> value containing the content of the specified file or url.</returns>
        internal static String GetContent(String pathOrUrl)
        {
            if (Uri.IsWellFormedUriString(pathOrUrl, UriKind.Absolute)) return FacebookApiGenerator.GetContentFromUri(pathOrUrl);
            else if (File.Exists(pathOrUrl)) return FacebookApiGenerator.GetContentFromFile(pathOrUrl);
            else return null;
        }

        /// <summary>Generates the .NET Facebook API client project.</summary>
        /// <param name="schemaPathOrUrl">The path or url of the Facebook API XML schema to use.</param>
        /// <param name="methodSignaturePathOrUrl">The path or url of the document containing method signature definitions.</param>
        /// <param name="language">The .NET language to generate the project in.</param>
        /// <param name="outputPath">The directory path to write the project to.</param>
        public static void Generate(String language, String outputPath)
        {
            var compileUnits = new List<CodeCompileUnit>();

            // create the "auto-generated" warning that will appear at the top of each file
            var assemblyName = new AssemblyName(Assembly.GetExecutingAssembly().FullName);
            var versionComponents = 2 +
                ((assemblyName.Version.Build > 0 || assemblyName.Version.Revision > 0) ? 1 : 0) +
                (assemblyName.Version.Revision > 0 ? 1 : 0);
            var generatedComment = new CodeCommentStatement(String.Format(Content.AutoGenerated, assemblyName.Version.ToString(versionComponents), ApiSourcePaths.ObjectSchema, ApiSourcePaths.WikiHome));

            // load schema content from the specified document and parse out the types.
            var schemaContent = FacebookApiGenerator.GetContent(ApiSourcePaths.ObjectSchema);
            using (var reader = new StringReader(schemaContent))
            {
                var parser = new FacebookApiSchemaParser(FacebookApiGenerator.GenerateMethodSignatures(), generatedComment);
                compileUnits.AddRange(parser.Parse(reader));
            }

            compileUnits.Add(FacebookApiGenerator.GenerateErrorCodesEnum());

            var outputBaseDir = new DirectoryInfo(outputPath);
            if (!outputBaseDir.Exists) outputBaseDir.Create();

            var baseNamespaceQuery =
                from compileUnit in compileUnits
                from ns in compileUnit.Namespaces.OfType<CodeNamespace>()
                orderby ns.Name.Split('.').Length
                select ns.Name;

            String baseNamespace = baseNamespaceQuery.Distinct().First();

            var generatorOptions = new CodeGeneratorOptions();

            var projectFile = new VsProjectFile(Path.Combine(outputBaseDir.FullName, "Facebook.csproj"));
            projectFile.RemoveGeneratedFileReferences();

            using (var provider = CodeDomProvider.CreateProvider(language))
            {
                var toCompile = new List<CompileItem>();
                
                foreach (var compileUnit in compileUnits)
                {
                    var ns = compileUnit.Namespaces[0].Name;
                    var namespaceRelativePath = ns.Replace(baseNamespace, String.Empty).Trim('.').Replace('.', '\\');
                    var namespacePath = Path.Combine(outputBaseDir.FullName, namespaceRelativePath);
                    var codeType = compileUnit.Namespaces[0].Types[0];
                    var fileName = codeType.Name + ".api.g." + provider.FileExtension;

                    String dependentFile;
                    var mergeType = projectFile.GetMergeType(Path.Combine(namespaceRelativePath, codeType.Name), out dependentFile);

                    if (mergeType == GeneratedMergeType.DoNotGenerate)
                    {
                        continue;
                    }
                    else if (mergeType == GeneratedMergeType.GeneratePartial)
                    {
                        codeType.IsPartial = true;
                    }

                    if (codeType.Name.Contains("Context") || codeType.Members.OfType<CodeTypeMember>().Any(member => member.GetType() != typeof(CodeConstructor)))
                    {
                        projectFile.AddFileReference(Path.Combine(namespaceRelativePath, fileName), dependentFile);
                        toCompile.Add(new CompileItem { CompileUnit = compileUnit, FileName = fileName, NamespacePath = namespacePath });
                    }
                }

                outputBaseDir.GetFiles("*.g.*", SearchOption.AllDirectories).Where(file => !file.FullName.Contains("svn")).ToList().ForEach(file => file.Delete());
                foreach (var compileItem in toCompile)
                {
                    if (!Directory.Exists(compileItem.NamespacePath)) Directory.CreateDirectory(compileItem.NamespacePath);

                    using (var file = File.Open(Path.Combine(compileItem.NamespacePath, compileItem.FileName), FileMode.Create))
                    {
                        using (var writer = new StreamWriter(file))
                        {
                            provider.GenerateCodeFromCompileUnit(compileItem.CompileUnit, writer, generatorOptions);                            
                        }
                    }
                }

                projectFile.Save();
            }
        }

        public static Dictionary<String, String> GetPageContent(IEnumerable<String> methodNames)
        {
            DateTime start = DateTime.Now;
            var dict = new Dictionary<String, String>();
            var queue = new Queue<String>(methodNames);
            var threads = new List<Thread>(30);

            while (queue.Count > 0)
            {
                while (threads.Count == threads.Capacity)
                {
                    if (threads.RemoveAll(t => (t.ThreadState & ThreadState.Background) != ThreadState.Background) == 0) Thread.Sleep(0);
                }
                var methodName = queue.Dequeue();

                var thread = new Thread(state =>
                {
                    var pageUrl = String.Format(ApiSourcePaths.MethodPageFormat, methodName);
                    var methodPageContent = FacebookApiGenerator.GetContent(pageUrl);
                    dict.Add(methodName, methodPageContent);
                    var thisThread = (Thread)state;
                    if (threads.Contains(thisThread)) threads.Remove((Thread)state);
                });
                thread.IsBackground = true;
                thread.Priority = ThreadPriority.BelowNormal;
                threads.Add(thread);
                thread.Start(thread);
            }

            while (threads.Count > 0)
            {
                if (threads.RemoveAll(t => t == null || (t.ThreadState & ThreadState.Background) != ThreadState.Background) == 0) Thread.Sleep(0);
            }
            var timeTaken = DateTime.Now - start;
            return dict;
        }
        
        public static Dictionary<String, List<MethodDefinition>> GenerateMethodSignatures()
        {
            var content = FacebookApiGenerator.GetContent(ApiSourcePaths.WikiHome);

            var matches = Regex.Matches(content, RegexPatterns.MethodIndex).OfType<Match>();
            var contentDict = GetPageContent(matches.Select(match => match.Groups["name"].Value));
            Dictionary<String, List<MethodDefinition>> methods = new Dictionary<String,List<MethodDefinition>>();
            Configuration.FacebookApiGeneratorSection config = Configuration.FacebookApiGeneratorSection.Current;
            foreach (Match match in matches)
            {   
                string excludeKey = match.Groups["name"].Value.Split('.').First().CapitalizeFirst() + "." + match.Groups["name"].Value.Split('.').Last().ToPascalCase();
                Configuration.MethodDefinitionElement excludeDefinition = null;
                Configuration.MethodDefinitionElement explicitDefinition = null;

                if (config.ExcludedMethods.Exists(excludeKey))
                    excludeDefinition = config.ExcludedMethods.GetMethod(excludeKey);
                if (config.ExplicitMethodDefinitions.Exists(excludeKey))
                    explicitDefinition = config.ExplicitMethodDefinitions.GetMethod(excludeKey);
                MethodParameterDefinitionElementCollection explicitParams = null;
                if (explicitDefinition != null && explicitDefinition.Overloads.Count > 0)
                    explicitParams = explicitDefinition.Overloads;

                if ((excludeDefinition != null && excludeDefinition.Overloads.Count > 0)||excludeDefinition == null) // We know that if there aren't any overloads, we won't generate the method at all
                {
                    MethodParameterDefinitionElementCollection excludedParams = null;
                    if (excludeDefinition != null && excludeDefinition.Overloads.Count > 0)
                        excludedParams = excludeDefinition.Overloads;

                    List<MethodOverloadDefinition> overloadList = GenerateMethodOverloads(match.Groups["name"].Value, match.Groups["param"].Captures.OfType<Capture>().Select(cap => cap.Value).ToArray(), contentDict, excludedParams, explicitParams);
                    methods.Add(match.Groups["name"].Value, new List<MethodDefinition>());
                    foreach (MethodOverloadDefinition overload in overloadList)
                    {
                        MethodDefinition method = new MethodDefinition
                        {
                            Name = match.Groups["name"].Value + (String.IsNullOrEmpty(overload.Suffix) ? String.Empty : overload.Suffix),
                            Description = match.Groups["description"].Value,
                            Parameters = overload.Parameters
                        };

                        methods[match.Groups["name"].Value].Add(method);
                    }
                }
            }
            return methods;
        }

        public static List<MethodOverloadDefinition> GenerateMethodOverloads(String methodName, String[] paramNames, 
            Dictionary<String, String> contentDict, MethodParameterDefinitionElementCollection excludedParams, MethodParameterDefinitionElementCollection explicitParams)
        {
            var commonParams = new String[] { "api_key", "call_id", "sig", "session_key", "v", "format", "callback" };

            var content = contentDict[methodName];
            var contentStart = content.IndexOf("<a name=\"Parameters\"></a>");
            var contentEnd = content.IndexOf("</table>", contentStart);
            content = content.Substring(contentStart, contentEnd - contentStart);

            var match = Regex.Match(content, RegexPatterns.MethodPage);
            var capsReq = match.Groups["required"].Captures.OfType<Capture>().ToList();
            var capsName = match.Groups["paramName"].Captures.OfType<Capture>().ToList();
            var capsType = match.Groups["paramType"].Captures.OfType<Capture>().ToList();
            var capsDescription = match.Groups["paramDescription"].Captures.OfType<Capture>().ToList();

            var indexes = new List<Int32>(capsReq.Count);
            for (var i = 0; i < indexes.Capacity; i++) indexes.Add(i);
                        
            var regexResult =
                from capReq in capsReq
                join capName in capsName on capsReq.IndexOf(capReq) equals capsName.IndexOf(capName)
                join capType in capsType on capsReq.IndexOf(capReq) equals capsType.IndexOf(capType)
                join capDescription in capsDescription on capsReq.IndexOf(capReq) equals capsDescription.IndexOf(capDescription)
                where !commonParams.Contains(capName.Value)
                select new
                {
                    IsRequired = capReq.Index < (match.Groups["required"].Captures.OfType<Capture>().Where(cap => cap.Value == "optional").Select(cap => (Int32?)cap.Index).FirstOrDefault() ?? Int32.MaxValue),
                    ParamName = capName.Value,
                    ParamType = GetMethodTypeName(capType.Value),
                    Description = capDescription.Value
                };

            List<MethodOverloadDefinition> result = new List<MethodOverloadDefinition>();

            var required =
                from param in regexResult
                where param.IsRequired
                select new MethodParameterDefinition { ApiParameterName = param.ParamName, DeclarationExpression = new CodeParameterDeclarationExpression(param.ParamType, param.ParamName.ToCamelCase()), Description = param.Description };
            var optional =
                from param in regexResult
                where !param.IsRequired
                select new MethodParameterDefinition { ApiParameterName = param.ParamName, DeclarationExpression = new CodeParameterDeclarationExpression(param.ParamType, param.ParamName.ToCamelCase()), Description = param.Description };

            List<MethodOverloadDefinition> overloadList = new List<MethodOverloadDefinition>();

            if (explicitParams != null) // If we have explicit definitions, generate only those overloads
            {
                var allParams = required.Union(optional);
                foreach (MethodParameterDefinitionElement explicitDef in explicitParams)
                {
                    List<MethodParameterDefinition> tmpList = new List<MethodParameterDefinition>();
                    if (!String.IsNullOrEmpty(explicitDef.Params))
                    {
                        foreach (string name in explicitDef.Params.Split(','))
                        {
                            tmpList.Add((from el in allParams
                                         where el.ApiParameterName == name
                                         select el).FirstOrDefault());
                        }
                    }
                    overloadList.Add(new MethodOverloadDefinition { ApplicationType = explicitDef.ApplicationType, Parameters = tmpList.ToArray(), Suffix = explicitDef.NameSuffix });
                }
            }
            else // Generate all possible overloads
            {
                List<MethodOverloadDefinition> permutations = PermuteParamList(optional.ToList());
                foreach (MethodOverloadDefinition permutation in permutations)
                {
                    List<MethodParameterDefinition> tempParams = permutation.Parameters.ToList();
                    tempParams.AddRange(required);
                    overloadList.Add(new MethodOverloadDefinition { Parameters = tempParams.ToArray() });
                }
                result.Add(new MethodOverloadDefinition { Parameters = required.ToArray() });
            }
            foreach (MethodOverloadDefinition overload in overloadList)
            {
                if (excludedParams == null)
                {
                    result.Add(overload);
                }
                else
                {
                    Boolean excluded = false;
                    var sortedParams = new List<String>(overload.Parameters.Select(p => p.ApiParameterName));
                    sortedParams.Sort();

                    foreach (MethodParameterDefinitionElement method in excludedParams)
                    {
                        if (method.ContainsParameters(sortedParams))
                            excluded = true;
                    }
                    if (!excluded)
                        result.Add(overload);
                }
            }
            return result;
        }

        public static String GetMethodTypeName(String apiTypeName)
        {
            Type type = null;
            try { type = ((XmlTypeCode)Enum.Parse(typeof(XmlTypeCode), apiTypeName.ToPascalCase())).ToType(); }
            catch { }

            if (type == null)
            {
                switch (apiTypeName)
                {
                    case "json array":

                    case "array": return "String[]";

                    case "bool": return "Boolean";

                    case "markup": return "String";

                    case "uid": return "Int64";

                    case "object": return "Dictionary<String, String>"; //Is this actually what we want to do here? (FIXME)
                }
                throw new ArgumentException(String.Format("Unrecognized API type: {0}", apiTypeName), "apiTypeName");
            }
            else return type.Name;
        }

        
        public static CodeCompileUnit GenerateErrorCodesEnum()
        {
            var content = FacebookApiGenerator.GetContent(ApiSourcePaths.ErrorCodes);

            var matches = Regex.Matches(content, @"(?s)<tr>\s*<td>\s*(?<code>\d+)\s*</td>\s*<td>\s*API_EC_(?<name>\w+)\s*</td>\s*<td>\s*(?<desc>.*?)\s*</td>\s*(?:<td>.*?</td>\s*)?</tr>");

            var members =
                from match in matches.OfType<Match>()
                select GenerateErrorCodeField(match.Groups["code"].Value, match.Groups["name"].Value.ToLower(), match.Groups["desc"].Value);

            var compileUnit = new CodeCompileUnit();
            var ns = new CodeNamespace("Facebook.Api");
            var codeType = new CodeTypeDeclaration("ErrorCode") { Attributes = MemberAttributes.Public, IsEnum = true };
            codeType.BaseTypes.Add(typeof(short));
            codeType.Members.AddRange(members.ToArray());

            ns.Types.Add(codeType);
            compileUnit.Namespaces.Add(ns);

            return compileUnit;
        }

        public static CodeMemberField GenerateErrorCodeField(String code, String name, String desc)
        {
            var field = new CodeMemberField(String.Empty, name.ToPascalCase());            
            field.InitExpression = new CodeSnippetExpression(code);
            field.Comments.Add(new CodeCommentStatement(String.Format("<summary>{0}</summary>", desc), true));

            return field;
        }

        public static List<MethodOverloadDefinition> PermuteParamList(List<MethodParameterDefinition> list)
        {
            List<MethodOverloadDefinition> accum = new List<MethodOverloadDefinition>();
            for (int i = 0; i < list.Count; i++)
            {
                List<MethodParameterDefinition> indexList = new List<MethodParameterDefinition>();
                indexList.Add(list[i]);
                accum.Add(new MethodOverloadDefinition { Parameters = indexList.ToArray() });
                for (int x = i + 1; x < list.Count; x++)
                {
                    var subList = list.SkipWhile((n, index) => index < x).ToList();
                    subList.Add(list[i]);
                    accum.Add(new MethodOverloadDefinition { Parameters = subList.ToArray() });
                }
            }
            return accum;
        }

    }
}